package com.chenwc.aliyunoss;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProvider;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.aliyun.oss.common.auth.ServiceSignature;
import com.aliyun.oss.common.comm.Protocol;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.*;
import com.chenwc.common.fileUtils.ReadFile;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author chenwc
 */
public class AliyunOssUtil {
    private static final Logger log = LoggerFactory.getLogger(AliyunOssUtil.class);

    private static OSS ossClient;
    private static String accessKey;
    private static String secretKey;

    private static EndpointEnum endpointEnum;

    /**
     * 断点续传分片上传，分片大小为1MB
     */
    private static final Integer BREAKPOINT_CONTINUATION_UPLOAD_PART_SIZE = 1 * 1024 * 1024;

    /**
     * 文件分块大小,10M
     */
    private static final Integer CHUNK_FILE_SIZE = 10 * 1024 * 1024;

    /**
     * 文件分块阈值,10M
     */
    private static final Integer CHUNK_FILE_SIZE_THRESHOLD = 10 * 1024 * 1024;

    /**
     * 设置ossClient属性
     *
     * @param userAccessKey    accessKey
     * @param userSecretKey    secretKey
     * @param userEndpointEnum endpointEnum
     */
    public static void getOssClient(String userAccessKey, String userSecretKey, EndpointEnum userEndpointEnum) {
        accessKey = userAccessKey;
        secretKey = userSecretKey;
        endpointEnum = userEndpointEnum;
    }

    /**
     * 获取ossClient
     */
    public static void getOssClient() {
        if (null == ossClient) {
            // 使用更安全的V4签名算法，则初始化时需要加入endpoint对应的region信息，同时声明SignVersion.V4。
            // OSS Java SDK 3.17.4及以上版本支持V4签名。
            // 以使用OSS域名新建OSSClient时使用V4签名为例，其他通过自定义域名、STS等新建OSSClient的场景可参考以下示例执行相应修改。
            ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
            clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);

            // ClientBuilderConfiguration是OSSClient的配置类，可配置代理、连接超时、最大连接数等参数。
            ClientBuilderConfiguration conf = getClientBuilderConfiguration();

            CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKey, secretKey);
            ossClient = OSSClientBuilder.create()
                    .endpoint(endpointEnum.getExternalEndpoint())
                    .credentialsProvider(credentialsProvider)
                    .clientConfiguration(conf)
                    .clientConfiguration(clientBuilderConfiguration)
                    .region(endpointEnum.getRegionId().replace("oss-", ""))
                    .build();
        }
    }

    /**
     * 获取ClientBuilderConfiguration
     */
    private static ClientBuilderConfiguration getClientBuilderConfiguration() {
        ClientBuilderConfiguration conf = new ClientBuilderConfiguration();

        // 设置OSSClient允许打开的最大HTTP连接数，默认为1024个。
        conf.setMaxConnections(200);
        // 设置Socket层传输数据的超时时间，默认为50000毫秒。
        conf.setSocketTimeout(10000);
        // 设置建立连接的超时时间，默认为50000毫秒。
        conf.setConnectionTimeout(10000);
        // 设置从连接池中获取连接的超时时间（单位：毫秒），默认不超时。
        conf.setConnectionRequestTimeout(1000);
        // 设置连接空闲超时时间。超时则关闭连接，默认为60000毫秒。
        conf.setIdleConnectionTime(10000);
        // 设置失败请求重试次数，默认为3次。
        conf.setMaxErrorRetry(5);
        // 设置是否支持将自定义域名作为Endpoint，默认支持。
        conf.setSupportCname(true);
        // 设置是否开启二级域名的访问方式，默认不开启。
        conf.setSLDEnabled(false);
        // 设置连接OSS所使用的协议（HTTP或HTTPS），默认为HTTP。
        conf.setProtocol(Protocol.HTTPS);
//            // 设置用户代理，指HTTP的User-Agent头，默认为aliyun-sdk-java。
//            conf.setUserAgent("aliyun-sdk-java");
//            // 设置代理服务器IP，请将"<yourProxyHost>"替换为代理服务器的IP地址（如"196.128.xxx.xxx"）。
//            conf.setProxyHost("<yourProxyHost>");
//            // 设置代理服务器验证的用户名，请将"<yourProxyUserName>"替换为代理服务器的用户名（如"root"）。
//            conf.setProxyUsername("<yourProxyUserName>");
//            // 设置代理服务器验证的密码，请将"<yourProxyPassword>"替换为该用户的验证密码。
//            conf.setProxyPassword("<yourProxyPassword>");
//            // 设置是否开启HTTP重定向，默认开启。
//            conf.setRedirectEnable(true);
//            // 设置是否开启SSL证书校验，默认开启。
//            conf.setVerifySSLEnable(true);
//            // 自定义重试策略，一般不建议设置。
//            // 假设TestRetryStrategy类为您自定义的重试策略，TestRetryStrategy类需要继承RetryStrategy。
//            conf.setRetryStrategy(new TestRetryStrategy());
        return conf;
    }

    /**
     * 关闭ossClient
     */
    public static void closeOssClient() {
        if (null != ossClient) {
            ossClient.shutdown();
        }
    }

    /**
     * 列举当前账号所有地域下的存储空间。
     *
     * @return 列举当前账号所有地域下的存储空间列表
     */
    public static JSONArray listBuckets() {
        getOssClient();
        try {
            // 列举当前账号所有地域下的存储空间。
            List<Bucket> buckets = ossClient.listBuckets();
            return JSONArray.parseArray(JSON.toJSONString(buckets));
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
        return new JSONArray();
    }

    /**
     * 判断存储空间是否存在
     *
     * @param bucketName 存储空间名称
     * @return 存在返回true，否则返回false
     */
    public static boolean isExistBucket(String bucketName) {
        getOssClient();
        try {
            return ossClient.doesBucketExist(bucketName);
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
        return false;
    }

    /**
     * 创建存储空间
     *
     * @param bucketName 存储空间名称
     */
    public static void createBucket(String bucketName) {
        try {
            // 创建CreateBucketRequest对象。
            CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
            // 如果创建存储空间的同时需要指定存储类型、存储空间的读写权限、数据容灾类型, 请参考如下代码。
            // 此处以设置存储空间的存储类型为标准存储为例介绍。
            //createBucketRequest.setStorageClass(StorageClass.Standard);
            // 数据容灾类型默认为本地冗余存储，即DataRedundancyType.LRS。如果需要设置数据容灾类型为同城冗余存储，请设置为DataRedundancyType.ZRS。
            //createBucketRequest.setDataRedundancyType(DataRedundancyType.ZRS);
            // 设置存储空间读写权限为公共读，默认为私有。
            //createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead);
            // 在支持资源组的地域创建Bucket时，您可以为Bucket配置资源组。
            //createBucketRequest.setResourceGroupId(rsId);
            // 创建存储空间。
            ossClient.createBucket(createBucketRequest);
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 获取存储空间的地域信息
     *
     * @param bucketName 存储空间名称
     * @return 存储空间的地域信息
     */
    public static String getBucketLocation(String bucketName) {
        getOssClient();
        try {
            // 获取存储空间的地域信息。
            return ossClient.getBucketLocation(bucketName);
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
        return "";
    }

    /**
     * 获取存储空间的信息
     *
     * @param bucketName 存储空间名称
     * @return 存储空间的信息
     */
    public static JSONObject getBucketInfo(String bucketName) {
        getOssClient();
        try {
            // 存储空间的信息包括地域（Region或Location）、创建日期（CreationDate）、拥有者（Owner）等。
            BucketInfo info = ossClient.getBucketInfo(bucketName);
            return JSONObject.parseObject(JSON.toJSONString(info));
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
        return new JSONObject();
    }

    /**
     * 获取存储空间的统计信息
     *
     * @param bucketName 存储空间名称
     * @return 存储空间的统计信息
     */
    public static JSONObject getBucketStat(String bucketName) {
        getOssClient();
        try {
            BucketStat stat = ossClient.getBucketStat(bucketName);
            return JSONObject.parseObject(JSON.toJSONString(stat));
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
        return new JSONObject();
    }

    /**
     * 获取存储空间的资源组ID
     *
     * @param bucketName 存储空间名称
     * @return 存储空间的资源组ID
     */
    public static String getResourceGroupId(String bucketName) {
        getOssClient();
        try {
            // 创建getBucketResourceGroupResult对象。
            GetBucketResourceGroupResult getBucketResourceGroupResult = ossClient.getBucketResourceGroup(bucketName);
            // 获取Bucket的资源组ID。
            return getBucketResourceGroupResult.getResourceGroupId();
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
        return "";
    }

    /**
     * 配置存储空间的资源组ID
     *
     * @param bucketName      存储空间名称
     * @param resourceGroupId 存储空间的资源组ID
     */
    public static void setBucketResourceGroup(String bucketName, String resourceGroupId) {
        getOssClient();
        try {
            // 创建setBucketResourceGroupRequest对象。
            SetBucketResourceGroupRequest setBucketResourceGroupRequest = new SetBucketResourceGroupRequest(bucketName, resourceGroupId);
            // 配置Bucket所属资源组。
            ossClient.setBucketResourceGroup(setBucketResourceGroupRequest);
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 删除存储空间
     *
     * @param bucketName 存储空间名称
     */
    public static void deleteBucket(String bucketName) {
        getOssClient();
        try {
            // 删除存储空间。
            ossClient.deleteBucket(bucketName);
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 为存储空间设置标签
     *
     * @param bucketName 存储空间名称
     * @param tags       存储空间的标签
     */
    public static void setBucketTagging(String bucketName, Map<String, String> tags) {
        getOssClient();
        try {
            // 创建SetBucketTaggingRequest对象。
            SetBucketTaggingRequest setBucketTaggingRequest = new SetBucketTaggingRequest(bucketName, tags);
            // 设置Bucket标签。
            ossClient.setBucketTagging(setBucketTaggingRequest);
        } catch (OSSException e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 上传文件
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @param localFile  本地文件
     */
    public static void uploadObject(String bucketName, String key, File localFile) {
        getOssClient();
        try {
//            // 方法1：创建上传文件流。
//            InputStream inputStream = Files.newInputStream(localFile.toPath());
//            // 创建PutObjectRequest对象。
//            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream);

//            // 方法2：直接上传字节数组
//            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key,
//                    new ByteArrayInputStream(Objects.requireNonNull(
//                            ReadFile.readFileByFileInputStream(localFile.getAbsolutePath()))
//                    )
//            );

            // 方法3：直接上传文件
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, localFile);

            // 创建上传文件的元信息。
            ObjectMetadata metadata = new ObjectMetadata();
            // 指定上传的内容类型。
            String contentType = new MimetypesFileTypeMap().getContentType(localFile);
            if (contentType == null || contentType.isEmpty()) {
                contentType = "application/octet-stream";
            }
            // 指定上传的内容类型。
            metadata.setContentType(contentType);

            // 如果需要上传时设置存储类型和访问权限，请参考以下示例代码。
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // metadata.setObjectAcl(CannedAccessControlList.Private);
            putObjectRequest.setMetadata(metadata);

            // 上传文件。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
            log.info("Etag: " + result.getETag());
            log.info("VersionId: " + result.getVersionId());
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 获取PostObject表单上传数据
     *
     * @param key       key
     * @param localFile 本地文件
     */
    public static Map<String, String> getPostObjectFormData(String key, File localFile) {
        // 设置表单Map。
        Map<String, String> formFields = new LinkedHashMap<>();
        // 设置文件名称。
        formFields.put("key", key);
        // 设置Content-Disposition。
        formFields.put("Content-Disposition", "attachment;filename="
                + localFile.getName());
        // 设置回调参数。
        // Callback callback = new Callback();
        // 设置回调服务器地址，例如http://oss-demo.oss-cn-hangzhou.aliyuncs.com:23450或http://127.0.0.1:9090。
        // callback.setCallbackUrl(callbackServerUrl);
        // 设置回调请求消息头中Host的值，如oss-cn-hangzhou.aliyuncs.com。
        // callback.setCallbackHost(callbackServerHost);
        // 设置发起回调时请求Body的值。
        // callback.setCallbackBody("{\\\"mimeType\\\":${mimeType},\\\"size\\\":${size}}");
        // 设置发起回调请求的Content-Type。
        // callback.setCalbackBodyType(Callback.CalbackBodyType.JSON);
        // 设置发起回调请求的自定义参数，由Key和Value组成，Key必须以x:开始，且必须小写。
        // callback.addCallbackVar("x:var1", "value1");
        // callback.addCallbackVar("x:var2", "value2");
        // 在表单Map中设置回调参数。
        // setCallBack(formFields, callback);
        // 设置OSSAccessKeyId。
        formFields.put("OSSAccessKeyId", accessKey);
        //设置前面过期时间和文件大小范围
        String policy = "{\"expiration\": \"2120-01-01T12:00:00.000Z\",\"conditions\": [[\"content-length-range\", 0, 104857600]]}";
        String encodePolicy = new String(Base64.encodeBase64(policy.getBytes()));
        // 设置policy。
        formFields.put("policy", encodePolicy);
        // 生成签名。
        String signaturecom = ServiceSignature.create().computeSignature(secretKey, encodePolicy);
        // 设置签名。
        formFields.put("Signature", signaturecom);
        return formFields;
    }

    /**
     * 断点续传上传文件
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @param localFile  本地文件
     */
    public static void breakpointContinuationUpload(String bucketName, String key, File localFile) {
        getOssClient();
        try {
            ObjectMetadata meta = new ObjectMetadata();
            String contentType = new MimetypesFileTypeMap().getContentType(localFile);
            if (contentType == null || contentType.isEmpty()) {
                contentType = "application/octet-stream";
            }
            // 指定上传的内容类型。
            meta.setContentType(contentType);

            // 文件上传时设置访问权限ACL。
            // meta.setObjectAcl(CannedAccessControlList.Private);

            // 通过UploadFileRequest设置多个参数。
            // 依次填写Bucket名称（例如examplebucket）以及Object完整路径（例如exampledir/exampleobject.txt），Object完整路径中不能包含Bucket名称。
            UploadFileRequest uploadFileRequest = new UploadFileRequest(bucketName, key);

            // 通过UploadFileRequest设置单个参数。
            // 填写本地文件的完整路径，例如D:\\localpath\\examplefile.txt。如果未指定本地路径，则默认从示例程序所属项目对应本地路径中上传文件。
            uploadFileRequest.setUploadFile(localFile.getAbsolutePath());
            // 指定上传并发线程数，默认值为1。
            uploadFileRequest.setTaskNum(5);
            // 指定上传的分片大小，单位为字节，取值范围为100 KB~5 GB。默认值为100 KB。
            uploadFileRequest.setPartSize(BREAKPOINT_CONTINUATION_UPLOAD_PART_SIZE);
            // 开启断点续传，默认关闭。
            uploadFileRequest.setEnableCheckpoint(true);
            // 记录本地分片上传结果的文件。上传过程中的进度信息会保存在该文件中，如果某一分片上传失败，再次上传时会根据文件中记录的点继续上传。上传完成后，该文件会被删除。
            // 如果未设置该值，默认与待上传的本地文件同路径，名称为${uploadFile}.ucp。
            uploadFileRequest.setCheckpointFile(localFile.getAbsolutePath() + ".ucp");
            // 文件的元数据。
            uploadFileRequest.setObjectMetadata(meta);
            // 设置上传回调，参数为Callback类型。
            //uploadFileRequest.setCallback("yourCallbackEvent");

            // 断点续传上传。
            ossClient.uploadFile(uploadFileRequest);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 上传文件（带进度条）
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @param localFile  本地文件
     */
    public static void putObjectWithProgressListener(String bucketName, String key, File localFile) {
        getOssClient();
        try {
            // 上传文件的同时指定进度条参数。
            ossClient.putObject(new PutObjectRequest(bucketName, key, localFile).
                    withProgressListener(new PutObjectProgressListener()));
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 分片上传文件
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @param localFile  本地文件
     */
    public static void multipartUploadFile(String bucketName, String key, File localFile) {
        getOssClient();
        InputStream inputStream = null;
        try {
            // 创建InitiateMultipartUploadRequest对象。
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);

            // 如果需要在初始化分片时设置请求头，请参考以下示例代码。
            ObjectMetadata metadata = new ObjectMetadata();
            String contentType = new MimetypesFileTypeMap().getContentType(localFile);
            if (contentType == null || contentType.isEmpty()) {
                contentType = "application/octet-stream";
            }
            metadata.setContentType(contentType);
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // 指定该Object的网页缓存行为。
            // metadata.setCacheControl("no-cache");
            // 指定该Object被下载时的名称。
            metadata.setContentDisposition("attachment;filename=" + localFile.getName());
            // 指定该Object的内容编码格式。
            // metadata.setContentEncoding(OSSConstants.DEFAULT_CHARSET_NAME);
            // 指定初始化分片上传时是否覆盖同名Object。此处设置为true，表示禁止覆盖同名Object。
            // metadata.setHeader("x-oss-forbid-overwrite", "true");
            // 指定上传该Object的每个part时使用的服务器端加密方式。
            // metadata.setHeader(OSSHeaders.OSS_SERVER_SIDE_ENCRYPTION, ObjectMetadata.KMS_SERVER_SIDE_ENCRYPTION);
            // 指定Object的加密算法。如果未指定此选项，表明Object使用AES256加密算法。
            // metadata.setHeader(OSSHeaders.OSS_SERVER_SIDE_DATA_ENCRYPTION, ObjectMetadata.KMS_SERVER_SIDE_ENCRYPTION);
            // 指定KMS托管的用户主密钥。
            // metadata.setHeader(OSSHeaders.OSS_SERVER_SIDE_ENCRYPTION_KEY_ID, "9468da86-3509-4f8d-a61e-6eab1eac****");
            // 指定Object的存储类型。
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard);
            // 指定Object的对象标签，可同时设置多个标签。
            // metadata.setHeader(OSSHeaders.OSS_TAGGING, "a:1");

            request.setObjectMetadata(metadata);

            // 初始化分片。
            InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
            // 返回uploadId。
            String uploadId = upresult.getUploadId();
            // 根据uploadId执行取消分片上传事件或者列举已上传分片的操作。
            // 如果您需要根据uploadId执行取消分片上传事件的操作，您需要在调用InitiateMultipartUpload完成初始化分片之后获取uploadId。
            // 如果您需要根据uploadId执行列举已上传分片的操作，您需要在调用InitiateMultipartUpload完成初始化分片之后，且在调用CompleteMultipartUpload完成分片上传之前获取uploadId。
            log.info("uploadId: {}", uploadId);

            // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
            List<PartETag> partETags = new ArrayList<>();
            // 每个分片的大小，用于计算文件有多少个分片。单位为字节。
            long partSize = CHUNK_FILE_SIZE;

            // 根据上传的数据大小计算分片数。以本地文件为例，说明如何通过File.length()获取上传数据的大小。
            long fileLength = localFile.length();
            //未达到分块文件阈值大小，走普通上传
            if (fileLength <= CHUNK_FILE_SIZE_THRESHOLD) {
                log.info("当前文件大小：{} 未达到分块文件阈值大小：{}，走普通上传！", fileLength, CHUNK_FILE_SIZE_THRESHOLD);
                uploadObject(bucketName, key, localFile);
                return;
            }
            int partCount = (int) (fileLength / partSize);
            if (fileLength % partSize != 0) {
                partCount++;
            }
            // 遍历分片上传。
            for (int i = 0; i < partCount; i++) {
                long startPos = i * partSize;
                long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(bucketName);
                uploadPartRequest.setKey(key);
                uploadPartRequest.setUploadId(uploadId);
                // 设置上传的分片流。
                // 以本地文件为例说明如何创建FIleInputstream，并通过InputStream.skip()方法跳过指定数据。
                inputStream = Files.newInputStream(localFile.toPath());
                inputStream.skip(startPos);
                uploadPartRequest.setInputStream(inputStream);
                // 设置分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
                uploadPartRequest.setPartSize(curPartSize);
                // 设置分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出此范围，OSS将返回InvalidArgument错误码。
                uploadPartRequest.setPartNumber(i + 1);
                log.info("正在上传第: {} 个分块...", i + 1);
                // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
                UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
                // 每次上传分片之后，OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
                partETags.add(uploadPartResult.getPartETag());
            }
            // 创建CompleteMultipartUploadRequest对象。
            // 在执行完成分片上传操作时，需要提供所有有效的partETags。OSS收到提交的partETags后，会逐一验证每个分片的有效性。当所有的数据分片验证通过后，OSS将把这些分片组合成一个完整的文件。
            CompleteMultipartUploadRequest completeMultipartUploadRequest =
                    new CompleteMultipartUploadRequest(bucketName, key, uploadId, partETags);

            // 如果需要在完成分片上传的同时设置文件访问权限，请参考以下示例代码。
            // completeMultipartUploadRequest.setObjectACL(CannedAccessControlList.Private);
            // 指定是否列举当前UploadId已上传的所有Part。仅在Java SDK为3.14.0及以上版本时，支持通过服务端List分片数据来合并完整文件时，将CompleteMultipartUploadRequest中的partETags设置为null。
            // Map<String, String> headers = new HashMap<String, String>();
            // 如果指定了x-oss-complete-all:yes，则OSS会列举当前UploadId已上传的所有Part，然后按照PartNumber的序号排序并执行CompleteMultipartUpload操作。
            // 如果指定了x-oss-complete-all:yes，则不允许继续指定body，否则报错。
            // headers.put("x-oss-complete-all","yes");
            // completeMultipartUploadRequest.setHeaders(headers);

            // 完成分片上传。
            CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
            log.info("uploadObjectMultipartUpload: {}", completeMultipartUploadResult.getETag());

        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                log.error("Error occurred: ", e);
            }
        }
    }

    /**
     * 取消分片上传。
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @param uploadId   uploadId
     */
    public static void abortMultipartUploadFile(String bucketName, String key, String uploadId) {
        getOssClient();
        try {
            // 取消分片上传。
            AbortMultipartUploadRequest abortMultipartUploadRequest =
                    new AbortMultipartUploadRequest(bucketName, key, uploadId);
            ossClient.abortMultipartUpload(abortMultipartUploadRequest);
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 获取指定uploadId下所有已经上传成功的分片。
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @param uploadId   uploadId
     * @return JSONArray
     */
    public static JSONArray listMultipartUploadFileParts(String bucketName, String key, String uploadId) {
        getOssClient();
        try {
            // 列举指定uploadId已上传的所有Part。
            ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, key, uploadId);
            // 设置分页时每一页中分片数量为100个。默认列举1000个分片。
            listPartsRequest.setMaxParts(100);
            // 指定List的起始位置。只有分片号大于此参数值的分片会被列举。
            listPartsRequest.setPartNumberMarker(2);
            PartListing partListing = ossClient.listParts(listPartsRequest);
            List<PartSummary> parts = partListing.getParts();
            JSONArray jsonArray = new JSONArray();
            for (PartSummary part : parts) {
                JSONObject partJson = JSONObject.parseObject(JSON.toJSONString(part, JSONWriter.Feature.WriteMapNullValue));
                jsonArray.add(partJson);
            }
            return jsonArray;
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
        return new JSONArray();
    }

    /**
     * 下载文件。
     *
     * @param bucketName     存储桶名称
     * @param key            key
     * @param outputFilePath 文件保存路径
     * @param versionId      版本ID，为空时下载最新版本
     */
    public static void getObject(String bucketName, String key, String outputFilePath, String versionId) {
        getOssClient();
        try {
            // ossObject包含文件所在的存储空间名称、文件名称、文件元数据以及一个输入流。
            OSSObject ossObject = ossClient.getObject(bucketName, key);
            //方法1：流式下载
            //WriteFile.writeFileByInputStream(ossObject.getObjectContent(), outputFilePath);
            // 封装GetObject请求。
            GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, key);
            if (StringUtils.isNotEmpty(versionId)) {
                getObjectRequest.setVersionId(versionId);
            }
            //方法2：下载到本地文件
            ossClient.getObject(getObjectRequest, new File(outputFilePath));

        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 判断文件是否存在。
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @return boolean
     */
    public static boolean doesObjectExist(String bucketName, String key) {
        getOssClient();
        try {
            return ossClient.doesObjectExist(bucketName, key);
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
        return false;
    }

    /**
     * 上传文件（带对象管理元数据）
     *
     * @param bucketName   存储桶名称
     * @param key          key
     * @param uploadFile   上传文件
     * @param userMetadata 用户自定义元数据
     */
    public static void putObjectByObjectMetadata(String bucketName, String key, File uploadFile,
                                                 Map<String, String> userMetadata) {
        getOssClient();
        try {
            // 创建上传文件的元数据。
            ObjectMetadata meta = new ObjectMetadata();
            String md5 = BinaryUtil.toBase64String(
                    BinaryUtil.calculateMd5(
                            ReadFile.readFileByFileInputStream(uploadFile.getAbsolutePath())
                    )
            );
            // 开启文件内容MD5校验。开启后OSS会把您提供的MD5与文件的MD5比较，不一致则抛出异常。
            meta.setContentMD5(md5);
            String contentType = new MimetypesFileTypeMap().getContentType(uploadFile);
            if (contentType == null || contentType.isEmpty()) {
                contentType = "application/octet-stream";
            }
            // 指定上传的内容类型。内容类型决定浏览器将以什么形式、什么编码读取文件。如果没有指定则根据文件的扩展名生成，如果没有扩展名则为默认值application/octet-stream。
            meta.setContentType(contentType);
            // 设置内容被下载时的名称。
            meta.setContentDisposition("attachment; filename=" + uploadFile.getName());
            // 设置上传文件的长度。如超过此长度，则上传文件会被截断，上传的文件长度为设置的长度。如小于此长度，则为上传文件的实际长度。
            meta.setContentLength(uploadFile.length());
            // 设置内容被下载时网页的缓存行为。
            meta.setCacheControl("no cache");
            // 设置缓存过期时间，默认3天
            long expirateTime = 3 * 24L * 3600L * 1000L;
            meta.setExpirationTime(new Date(System.currentTimeMillis() + expirateTime));
            // 设置内容被下载时的编码格式。
            meta.setContentEncoding("gzip");
            // 设置用户自定义元数据
            if (userMetadata != null && !userMetadata.isEmpty()) {
                meta.setUserMetadata(userMetadata);
            }
            // 上传文件。
            ossClient.putObject(bucketName, key, uploadFile, meta);
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 修改文件元数据
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @param meta       元数据
     */
    public static void setNewObjectMetadata(String bucketName, String key, ObjectMetadata meta) {
        getOssClient();
        try {
            // 设置源文件与目标文件相同，调用ossClient.copyObject方法修改文件元数据。
            CopyObjectRequest request = new CopyObjectRequest(bucketName, key, bucketName, key);
            request.setNewObjectMetadata(meta);
            // 修改元数据。
            ossClient.copyObject(request);
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 获取文件元数据
     *
     * @param bucketName 存储桶名称
     * @param key        key
     * @return JSONObject
     */
    public static ObjectMetadata getObjectMetadata(String bucketName, String key) {
        getOssClient();
        try {
            // 获取文件的部分元数据。
            //SimplifiedObjectMeta objectMeta = ossClient.getSimplifiedObjectMeta(bucketName, key);
            // 获取文件元数据。
            return ossClient.getObjectMetadata(bucketName, key);
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        }
        return null;
    }

    /**
     * 列举文件
     *
     * @param bucketName 存储桶名称
     * @param keyPrefix  key前缀
     * @return List<OSSObjectSummary>
     */
    public static List<OSSObjectSummary> listObjects(String bucketName, String keyPrefix) {
        getOssClient();
        try {
            // 列举文件。如果不设置keyPrefix，则列举存储空间下的所有文件。如果设置keyPrefix，则列举包含指定前缀的文件。
            ObjectListing objectListing = ossClient.listObjects(bucketName, keyPrefix);
            return objectListing.getObjectSummaries();
        } catch (OSSException oe) {
            log.error("Error occurred: ", oe);
        }
        return null;
    }

    /**
     * 列举文件V2
     *
     * @param bucketName 存储桶名称
     * @param keyPrefix  key前缀
     * @return List<OSSObjectSummary>
     */
    public static List<OSSObjectSummary> listObjectsV2(String bucketName, String keyPrefix) {
        getOssClient();
        try {
            // 列举文件。如果不设置keyPrefix，则列举存储空间下的所有文件。如果设置keyPrefix，则列举包含指定前缀的文件。
            ListObjectsV2Result result = ossClient.listObjectsV2(bucketName, keyPrefix);
            return result.getObjectSummaries();
        } catch (OSSException oe) {
            log.error("Error occurred: ", oe);
        }
        return null;
    }

    /**
     * 分页列举所有文件
     *
     * @param bucketName 存储桶名称
     * @param keyPrefix  key前缀
     * @param maxKeys    每页列举的最大文件数
     * @return List<OSSObjectSummary>
     */
    public static List<OSSObjectSummary> listAllObjectsByPage(String bucketName, String keyPrefix, int maxKeys) {
        getOssClient();
        List<OSSObjectSummary> list = new ArrayList<>();
        try {
            String nextMarker = null;
            ObjectListing objectListing;
            do {
                objectListing = ossClient.listObjects(
                        new ListObjectsRequest(bucketName)
                                .withMarker(nextMarker)
                                .withMaxKeys(maxKeys)
                                .withPrefix(keyPrefix)
                );
                List<OSSObjectSummary> sums = objectListing.getObjectSummaries();
                list.addAll(sums);
                nextMarker = objectListing.getNextMarker();
            } while (objectListing.isTruncated());
        } catch (OSSException oe) {
            log.error("Error occurred: ", oe);
        }
        return list;
    }

    /**
     * 分页列举所有文件V2
     *
     * @param bucketName 存储桶名称
     * @param keyPrefix  key前缀
     * @param maxKeys    每页列举的最大文件数
     * @return List<OSSObjectSummary>
     */
    public static List<OSSObjectSummary> listAllObjectsV2ByPage(String bucketName, String keyPrefix, int maxKeys) {
        getOssClient();
        List<OSSObjectSummary> list = new ArrayList<>();
        try {
            String nextContinuationToken = null;
            ListObjectsV2Result result;
            // 分页列举，每次传入上次返回结果中的nextContinuationToken。
            do {
                ListObjectsV2Request listObjectsV2Request = new ListObjectsV2Request(bucketName)
                        .withPrefix(keyPrefix)
                        .withMaxKeys(maxKeys);
                listObjectsV2Request.setContinuationToken(nextContinuationToken);
                result = ossClient.listObjectsV2(listObjectsV2Request);
                List<OSSObjectSummary> sums = result.getObjectSummaries();
                list.addAll(sums);
                nextContinuationToken = result.getNextContinuationToken();
            } while (result.isTruncated());
        } catch (OSSException oe) {
            log.error("Error occurred: ", oe);
        }
        return list;
    }

    /**
     * 查询CSV文件
     *
     * @param bucketName    存储桶名称
     * @param key           对象名称
     * @param sqlExpression sql表达式
     * @return 查询结果
     */
    public static String selectCsvObject(String bucketName, String key, String sqlExpression) {
        getOssClient();
        BufferedReader reader = null;
        if (!key.toLowerCase().endsWith(".csv")) {
            log.error("文件不是CSV格式");
            return null;
        }
        try {
            // 创建查询文件元数据请求
            SelectObjectMetadata selectObjectMetadata = ossClient.createSelectObjectMetadata(
                    new CreateSelectObjectMetadataRequest(bucketName, key)
                            .withInputSerialization(
                                    new InputSerialization().withCsvInputFormat(
                                            // 填写内容中不同记录之间的分隔符，例如\r\n。
                                            new CSVFormat().withHeaderInfo(CSVFormat.Header.Use).withRecordDelimiter("\r\n"))));
            log.info("文件总行数：" + selectObjectMetadata.getCsvObjectMetadata().getTotalLines());

            // 创建查询请求。
            SelectObjectRequest selectObjectRequest =
                    new SelectObjectRequest(bucketName, key)
                            .withInputSerialization(
                                    new InputSerialization().withCsvInputFormat(
                                            new CSVFormat().withHeaderInfo(CSVFormat.Header.Use).withRecordDelimiter("\r\n")))
                            .withOutputSerialization(new OutputSerialization().withCsvOutputFormat(new CSVFormat()));

            selectObjectRequest.setExpression(sqlExpression);
            OSSObject ossObject = ossClient.selectObject(selectObjectRequest);

            // 读取内容。
            reader = new BufferedReader(new InputStreamReader(ossObject.getObjectContent()));
            StringBuilder sb = new StringBuilder();
            while (true) {
                String line = reader.readLine();
                if (line == null) {
                    break;
                }
                sb.append(line).append("\r\n");
            }
            reader.close();
            return sb.toString();
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
        return null;
    }

    /**
     * 查询JSON文件
     *
     * @param bucketName    存储桶名称
     * @param key           对象名称
     * @param sqlExpression sql表达式
     * @return 查询结果
     */
    public static String selectJsonObject(String bucketName, String key, String sqlExpression) {
        getOssClient();
        BufferedReader reader = null;
        if (!key.toLowerCase().endsWith(".json")) {
            log.error("文件不是JSON格式");
            return null;
        }
        try {
            SelectObjectRequest selectObjectRequest =
                    new SelectObjectRequest(bucketName, key)
                            .withInputSerialization(new InputSerialization()
                                    .withCompressionType(CompressionType.NONE)
                                    .withJsonInputFormat(new JsonFormat().withJsonType(JsonType.LINES)))
                            .withOutputSerialization(new OutputSerialization()
                                    .withCrcEnabled(true)
                                    .withJsonOutputFormat(new JsonFormat()))
                            // 使用SELECT语句查询文件中的数据。
                            .withExpression(sqlExpression);

            OSSObject ossObject = ossClient.selectObject(selectObjectRequest);

            // 读取内容。
            reader = new BufferedReader(new InputStreamReader(ossObject.getObjectContent()));
            StringBuilder sb = new StringBuilder();
            while (true) {
                String line = reader.readLine();
                if (line == null) {
                    break;
                }
                sb.append(line).append("\r\n");
            }
            return sb.toString();
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
        return null;
    }

    /**
     * 重命名对象
     *
     * @param bucketName 存储桶名称
     * @param key        对象名称
     * @param newKey     新对象名称
     */
    public static void renameObject(String bucketName, String key, String newKey) {
        getOssClient();
        try {
            ossClient.copyObject(bucketName, key, bucketName, newKey);
            // 删除src object
            ossClient.deleteObject(bucketName, key);
        } catch (OSSException oe) {
            log.error("Error occurred: ", oe);
        }
    }

    /**
     * 删除对象
     *
     * @param bucketName 存储桶名称
     * @param key        对象名称
     * @param versionId  对象版本ID，为空时删除最新版本
     */
    public static void deleteObject(String bucketName, String key, String versionId) {
        getOssClient();
        if (StringUtils.isNotEmpty(versionId)) {
            ossClient.deleteVersion(bucketName, key, versionId);
        } else {
            // 删除文件或目录。如果要删除目录，目录必须为空。
            ossClient.deleteObject(bucketName, key);
        }
    }

    /**
     * 批量删除对象
     *
     * @param bucketName 存储桶名称
     * @param versions   对象版本列表
     */
    public static void deleteObjectListByVersion(String bucketName, List<Map<String, String>> versions) {
        getOssClient();
        try {
            // 删除指定版本的Object与删除标记。
            List<DeleteVersionsRequest.KeyVersion> keyVersionsList = new ArrayList<>();
            for (Map<String, String> version : versions) {
                String key = version.get("key");
                String versionId = version.get("versionId");
                keyVersionsList.add(new DeleteVersionsRequest.KeyVersion(key, versionId));
            }
            DeleteVersionsRequest delVersionsRequest = new DeleteVersionsRequest(bucketName);
            delVersionsRequest.setKeys(keyVersionsList);
            // 发起deleteVersions请求。
            DeleteVersionsResult delVersionsResult = ossClient.deleteVersions(delVersionsRequest);

            // 查看删除结果。
            for (DeleteVersionsResult.DeletedVersion delVer : delVersionsResult.getDeletedVersions()) {
                String keyName = URLDecoder.decode(delVer.getKey(), "UTF-8");
                String keyVersionId = delVer.getVersionId();
                String keyDelMarkerId = delVer.getDeleteMarkerVersionId();
                if (keyDelMarkerId != null && !keyDelMarkerId.isEmpty()) {
                    log.info("delete key del_marker versionId: " + keyDelMarkerId);
                }
            }
        } catch (Exception oe) {
            log.error("Error occurred: ", oe);
        }
    }

    /**
     * 批量删除对象
     *
     * @param bucketName 存储桶名称
     * @param keys       对象名称列表
     */
    public static void deleteObjectList(String bucketName, List<String> keys) {
        getOssClient();
        try {
            DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(
                    new DeleteObjectsRequest(bucketName).withKeys(keys).withEncodingType("url"));
            List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
            try {
                for (String obj : deletedObjects) {
                    String deleteObj = URLDecoder.decode(obj, "UTF-8");
                    log.info("deleteObjUrl: " + deleteObj);
                }
            } catch (UnsupportedEncodingException e) {
                log.error("Error occurred: ", e);
            }
        } catch (OSSException oe) {
            log.error("Error occurred: ", oe);
        }
    }

    /**
     * 删除指定前缀或目录下的多个文件
     *
     * @param bucketName 存储桶名称
     * @param keyPrefix  指定前缀或目录
     */
    public static void deleteObjectListWithPrefixByVersion(String bucketName, String keyPrefix) {
        getOssClient();
        try {
            // 列举所有指定前缀文件的版本信息并删除这些文件。
            String nextKeyMarker = null;
            String nextVersionMarker = null;
            VersionListing versionListing = null;
            do {
                ListVersionsRequest listVersionsRequest = new ListVersionsRequest()
                        .withBucketName(bucketName)
                        .withKeyMarker(nextKeyMarker)
                        .withVersionIdMarker(nextVersionMarker)
                        .withPrefix(keyPrefix);

                versionListing = ossClient.listVersions(listVersionsRequest);
                if (!versionListing.getVersionSummaries().isEmpty()) {
                    List<DeleteVersionsRequest.KeyVersion> keyVersionsList = new ArrayList<>();
                    for (OSSVersionSummary ossVersion : versionListing.getVersionSummaries()) {
                        keyVersionsList.add(new DeleteVersionsRequest.KeyVersion(ossVersion.getKey(), ossVersion.getVersionId()));
                    }
                    DeleteVersionsRequest delVersionsRequest = new DeleteVersionsRequest(bucketName).withKeys(keyVersionsList);
                    ossClient.deleteVersions(delVersionsRequest);
                }
                nextKeyMarker = versionListing.getNextKeyMarker();
                nextVersionMarker = versionListing.getNextVersionIdMarker();
            } while (versionListing.isTruncated());
        } catch (Exception oe) {
            log.error("Error occurred: ", oe);
        }
    }

    /**
     * 删除指定前缀或目录下的多个文件
     *
     * @param bucketName 存储桶名称
     * @param keyPrefix  指定前缀或目录
     */
    public static void deleteObjectListWithPrefix(String bucketName, String keyPrefix) {
        getOssClient();
        try {
            // 列举所有包含指定前缀的文件并删除。
            String nextMarker = null;
            ObjectListing objectListing;
            do {
                ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName)
                        .withPrefix(keyPrefix)
                        .withMarker(nextMarker);
                objectListing = ossClient.listObjects(listObjectsRequest);
                if (!objectListing.getObjectSummaries().isEmpty()) {
                    List<String> keys = new ArrayList<>();
                    for (OSSObjectSummary s : objectListing.getObjectSummaries()) {
                        keys.add(s.getKey());
                    }
                    DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucketName).withKeys(keys).withEncodingType("url");
                    DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(deleteObjectsRequest);
                    List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
                    try {
                        for (String obj : deletedObjects) {
                            String deleteObj = URLDecoder.decode(obj, "UTF-8");
                            log.info("deleteObjUrl: " + deleteObj);
                        }
                    } catch (UnsupportedEncodingException e) {
                        log.error("Error occurred: ", e);
                    }
                }
                nextMarker = objectListing.getNextMarker();
            } while (objectListing.isTruncated());
        } catch (OSSException oe) {
            log.error("Error occurred: ", oe);
        }
    }

    /**
     * 获取存储桶版本控制状态
     *
     * @param bucketName 存储桶名称
     * @return BucketVersioningConfiguration
     */
    public static BucketVersioningConfiguration getBucketVersioningConfiguration(String bucketName) {
        getOssClient();
        return ossClient.getBucketVersioning(bucketName);
    }

    /**
     * 设置存储桶版本控制状态
     *
     * @param bucketName    存储桶名称
     * @param configuration BucketVersioningConfiguration
     */
    public static void setBucketVersioning(String bucketName, BucketVersioningConfiguration configuration) {
        getOssClient();
        ossClient.setBucketVersioning(new SetBucketVersioningRequest(bucketName, configuration));
    }

    /**
     * 获取指定Object的版本信息
     *
     * @param bucketName 存储桶名称
     * @param key        对象名称
     * @return List<OSSVersionSummary>
     */
    public static List<OSSVersionSummary> getObjectVersionList(String bucketName, String key) {
        getOssClient();
        List<OSSVersionSummary> ossVersionSummaryList = new ArrayList<>();
        try {
            // 列举包括删除标记在内的所有Object的版本信息。
            String nextKeyMarker = null;
            String nextVersionMarker = null;
            VersionListing versionListing = null;
            do {
                ListVersionsRequest listVersionsRequest = new ListVersionsRequest()
                        .withBucketName(bucketName)
                        .withKeyMarker(nextKeyMarker)
                        .withPrefix(key)
                        .withVersionIdMarker(nextVersionMarker);

                versionListing = ossClient.listVersions(listVersionsRequest);
                ossVersionSummaryList.addAll(versionListing.getVersionSummaries());
                nextKeyMarker = versionListing.getNextKeyMarker();
                nextVersionMarker = versionListing.getNextVersionIdMarker();
            } while (versionListing.isTruncated());
        } catch (OSSException oe) {
            log.error("Error occurred: ", oe);
        }
        return ossVersionSummaryList;
    }

    /**
     * 获取指定Object的版本号
     *
     * @param bucketName 存储桶名称
     * @param key        对象名称
     * @return String
     */
    public static String getObjectVersionId(String bucketName, String key) {
        getOssClient();
        return ossClient.getObjectMetadata(bucketName, key).getVersionId();
    }

    /**
     * 获取指定Object的前面URL
     *
     * @param bucketName       存储桶名称
     * @param key              对象名称
     * @param expirationTime   过期时间
     * @param unit             时间单位
     * @return String
     */
    public static String getObjectPresignedUrl(String bucketName, String key, long expirationTime, TimeUnit unit) {
        getOssClient();
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, key, HttpMethod.GET);
        long expirateTime;
        switch (unit) {
            case MILLISECONDS:
                expirateTime = expirationTime;
                break;
            case SECONDS:
                expirateTime = expirationTime * 1000L;
                break;
            case MINUTES:
                expirateTime = expirationTime * 60L * 1000L;
                break;
            case HOURS:
                expirateTime = expirationTime * 3600L * 1000L;
                break;
            case DAYS:
                expirateTime = expirationTime * 24L * 3600L * 1000L;
                break;
            default:
                throw new RuntimeException("时间单位不对，取值范围是：MILLISECONDS、SECONDS、MINUTES、HOURS、DAYS");
        }
        Date expirationDate = new Date(System.currentTimeMillis() + expirateTime);
        request.setExpiration(expirationDate);
        return ossClient.generatePresignedUrl(request).toString();
    }
}
